package winter.com.ideaaedi.classwinter.executor;

import winter.com.ideaaedi.classwinter.author.JustryDeng;
import winter.com.ideaaedi.classwinter.exception.ClassWinterException;
import winter.com.ideaaedi.classwinter.util.Cache;
import winter.com.ideaaedi.classwinter.util.Constant;
import winter.com.ideaaedi.classwinter.util.EncryptUtil;
import winter.com.ideaaedi.classwinter.util.IOUtil;
import winter.com.ideaaedi.classwinter.util.JarUtil;
import winter.com.ideaaedi.classwinter.util.Logger;
import winter.com.ideaaedi.classwinter.util.Pair;
import winter.com.ideaaedi.classwinter.util.StrUtil;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipFile;

/**
 * java class解密
 *
 * @author {@link JustryDeng}
 * @since 2021/4/28 22:07:51
 */
public class DecryptExecutor {
    
    /** 要解密的文件清单，记录已加密的class的全类名的清单文件 */
    private static Set<String> checklist;
    
    /**
     * 以key-value的形式记录lib中的被混淆的类(key-被混淆的类的无后缀全类名， value-存放采集到的类所在lib的信息的文件夹)。
     *
     * key   - 形如: com.abc.xyz.Qwer
     * value - 形如: META-INF/winter/abc-1.0.0_jar/
     */
    private static Map<String, String> checklistOfAllLibsMap;

    
    /**
     * 根据名称解密出一个文件(，得到该文件的byte[])
     *
     * @param projectPath
     *            jar/war文件或目录（如: /tmp/, /tmp/abc.jar等）
     * @param classWinterInfoDir
     *            class-winter相关信息的存储目录（如果不指定，则走默认的目录）
     * @param classLongName
     *            class全类名（如: com。niantou.winter.core.Abc）
     * @param password
     *            密码
     * @return  解压后的字节(null-表示classLongName所代表的文件不应该被解密)
     */
    public static byte[] process(String projectPath, String classWinterInfoDir, String classLongName, char[] password) {
        String baseWinterInfoDir = StrUtil.isBlank(classWinterInfoDir) ? Constant.DEFAULT_ENCRYPTED_BASE_SAVE_DIR : classWinterInfoDir;
        classWinterInfoDir = StrUtil.isBlank(classWinterInfoDir) ? Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR : classWinterInfoDir;
        // 读取文件
        byte[] bytes = IOUtil.readFileFromWorkbenchRoot(new File(projectPath), classWinterInfoDir + classLongName);
        // 检查密码一致性
        boolean userIfInputPwdWhileDecrypt = checkPwdConsistency(projectPath, baseWinterInfoDir, password);
        // 解密
        return EncryptUtil.decrypt(bytes, userIfInputPwdWhileDecrypt ? password :
                obtainAutoGeneratedPwdWhileEncrypt(projectPath, baseWinterInfoDir));
    }
    
    /**
     * 判断classLongName是否应该被解密
     * <p>
     *     即: 若classLongName对应的类在加密清单中，说明该文件是加密后的文件，那么就需要对其进行解密；否者不需要进行解密。
     * </p>
     *
     * @param projectPath
     *            jar/war文件或目录（如: /tmp/, /tmp/abc.jar等）
     * @param classLongName
     *            class全类名（如: com。niantou.winter.core.Abc）
     * @return classLongName是否在(记录已加密类的)清单列表中
     */
    public static boolean checklistContain(String projectPath, String classLongName) {
        if (checklist == null) {
            byte[] checklistByte = IOUtil.readFileFromWorkbenchRoot(new File(projectPath),
                    Constant.ALREADY_ENCRYPTED_CLASS_CHECKLIST_CLASSES_SAVE_FILE);
            if (checklistByte == null) {
                checklist = new HashSet<>();
                Logger.debug(DecryptExecutor.class, "checklistByte is null. ");
            } else {
                String checklistContent = new String(checklistByte, StandardCharsets.UTF_8);
                Logger.debug(DecryptExecutor.class, "checklistContent " + checklistContent);
                checklist = new HashSet<>(Arrays.asList(checklistContent.split(Constant.COMMA)));
            }
        }
        return checklist.contains(classLongName);
    }
    
    /**
     * 判断classLongName是否应该被解密（判断依据是: 本项目所依赖的所有lib包中的checklist，如果有的话）
     *
     * @param projectPath
     *            jar/war文件或目录（如: /tmp/, /tmp/abc.jar等）
     * @param classLongName
     *            无后缀的class全类名（如: com。niantou.winter.core.Abc）
     * @return classLongName是否在(记录已加密类的)清单列表中
     */
    public static boolean checklistOfAllLibsContain(String projectPath, String classLongName) {
        if (checklistOfAllLibsMap == null) {
            byte[] checklistOfAllLibsByte = IOUtil.readFileFromWorkbenchRoot(new File(projectPath),
                    Constant.CHECKLIST_OF_ALL_LIBS);
            if (checklistOfAllLibsByte == null) {
                checklistOfAllLibsMap = new HashMap<>(1);
                Logger.debug(DecryptExecutor.class, "checklistOfAllLibsByte is null. ");
            } else {
                checklistOfAllLibsMap = new HashMap<>(128);
                String checklistOfAllLibsContent = new String(checklistOfAllLibsByte, StandardCharsets.UTF_8);
                Logger.debug(DecryptExecutor.class, "checklistOfAllLibsContent " + checklistOfAllLibsContent);
                Arrays.stream(checklistOfAllLibsContent.split(Constant.LINE_SEPARATOR))
                        .filter(str -> !StrUtil.isBlank(str))
                        .forEach(info -> {
                            int idx = info.indexOf("=");
                            if (idx > 0) {
                                // libDirRelativePath形如: META-INF/winter/classes/abc-1.0.0_jar/
                                String libDirRelativePath = info.substring(0, idx);
                                String checklist = info.substring(idx + 1);
                                if (StrUtil.isBlank(checklist)) {
                                    throw new ClassWinterException("lib["
                                            + libDirRelativePath.substring(0, libDirRelativePath.length() - 4) + Constant.JAR_SUFFIX
                                            + "]'s checklist is blank.");
                                }
                                Arrays.stream(checklist.split(","))
                                        .map(String::trim)
                                        .forEach(nonSuffixClassLongName -> checklistOfAllLibsMap.put(nonSuffixClassLongName, libDirRelativePath));
                            } else {
                                throw new ClassWinterException("checklistOfAllLibsContent Incorrect format.");
                            }
                        });
            }

        }
        return checklistOfAllLibsMap.containsKey(classLongName);
    }
    
    /**
     * 根据类名获取，其所在lib对应的class-winter信息存储目录
     *
     * @param classLongName
     *            无后缀全类名(形如 com.abc.xyz.Qwer)
     * @return  存放采集到的类所在lib的信息的文件夹(形如 META-INF/winter/abc-1.0.0_jar/)
     */
    public static String getLibDirRelativePath(String classLongName) {
        Objects.requireNonNull(checklistOfAllLibsMap);
        return checklistOfAllLibsMap.get(classLongName);
    }
    
    /**
     * 判断类classBytes是否被盖章
     *
     * @param classBytes
     *            类的字节码
     * @return classBytes中是否存在加密印章
     */
    public static boolean verifySeal(byte[] classBytes) {
        return new String(classBytes, StandardCharsets.UTF_8).contains(Cache.sealCache);
    }
    
    /**
     * 判断类classBytes是否被盖章
     *
     * @param projectPath
     *            jar/war文件或目录（如: /tmp/, /tmp/abc.jar等）
     * @param classLongName
     *            无后缀的class全类名（如: com。niantou.winter.core.Abc）
     * @param classBytes
     *            类的字节码
     * @return classBytes中是否存在加密印章
     */
    public static boolean verifyLibSeal(String projectPath, String classLongName, byte[] classBytes) {
        if (Cache.libSealCache == null) {
            Cache.libSealCache = new HashMap<>(8);
            checklistOfAllLibsMap.values().stream().distinct().forEach(libDirRelativePath -> {
                byte[] sealByte = IOUtil.readFileFromWorkbenchRoot(new File(projectPath),
                        libDirRelativePath + Constant.SEAL_FILE_SIMPLE_NAME);
                if (sealByte == null) {
                    throw new ClassWinterException("Lib " + libDirRelativePath.substring(0, libDirRelativePath.length() - 4)
                            + Constant.JAR_SUFFIX + " missing seal information.");
                } else {
                    String libSealContent = new String(sealByte, StandardCharsets.UTF_8);
                    Logger.debug(DecryptExecutor.class,
                            "seal(from lib[" + DecryptExecutor.parseLib(libDirRelativePath) + "]) is " + libSealContent);
                    Cache.libSealCache.put(libDirRelativePath, libSealContent);
                }
            });
        }
    
        // 当且仅当checklistOfAllLibsMap.containsKey(classLongName)为true时才会调用此方法，所以这里libDirRelativePath一定有值的
        String libDirRelativePath = getLibDirRelativePath(classLongName);
        String sealContent = Cache.libSealCache.get(libDirRelativePath);
        return new String(classBytes, StandardCharsets.UTF_8).contains(sealContent);
    }
    
    /**
     * 检查密码一致性，即: 当加密时，用户输入了密码，那么解密时就要求用户输入密码
     *
     * @param projectPath
     *            jar/war文件或目录（如: /tmp/, /tmp/abc.jar等）
     * @param classWinterInfoDir
     *            class-winter相关信息的存储目录
     * @param password
     *            密码
     *
     * @return 在启动项目时(解密时)，用户是否输入了密码
     */
    private static boolean checkPwdConsistency(String projectPath, String classWinterInfoDir, char[] password) {
        // 查看加密时，用户是否主动输入了密码
        boolean userIfInputPwdWhileEncrypt = Boolean.parseBoolean(
                new String(IOUtil.readFileFromWorkbenchRoot(new File(projectPath),
                classWinterInfoDir + Constant.USER_IF_INPUT_PWD_SIMPLE_NAME))
        );
        // 解密时，用户是否主动输入了密码
        boolean userIfInputPwdWhileDecrypt = !StrUtil.isEmpty(password);
        // 如果加密时输入了密码，那么解密时就要求用户输入密码
        if (userIfInputPwdWhileEncrypt && !userIfInputPwdWhileDecrypt) {
            throw new ClassWinterException("Please input password while starting project.");
        }
        return userIfInputPwdWhileDecrypt;
    }
    
    /**
     * 根据classWinterInfoDir解析lib名称
     * <p>
     *     如: META-INF/winter/abc-1.0.0_jar/解析出来的lib为abc-1.0.0.jar
     * </p>
     *
     * @param classWinterInfoDir
     *            lib对应的class-winter信息存储目录， 形如META-INF/winter/abc-1.0.0_jar/
     * @return lib名称
     */
    public static String parseLib(String classWinterInfoDir) {
        classWinterInfoDir = classWinterInfoDir.replace(Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR, "");
        return classWinterInfoDir.replace("_jar/", Constant.JAR_SUFFIX);
    }
    
    /**
     * 恢复非class文件的混淆
     *
     * @param zipFilePath
     *            项目jar/war包路径
     * @param password
     *            项目加密密码（可能为null）
     * @return  被替换了的ZipEntry的相对路径及重写前后的内容信息<br/>
     *          k - ZipEntry的相对路径，如：BOOT-INF/classes/application.yml<br/>
     *          v - 左：重写前的内容，右：重写后的内容
     */
    public static Map<String, Pair<byte[], byte[]>> unMaskNonClassFiles(String zipFilePath, char[] password) throws IOException {
        final File zipFile = new File(zipFilePath);
        byte[] nonClassFileChecklistBytes = IOUtil.readFileFromWorkbenchRoot(zipFile, Constant.ALREADY_ENCRYPTED_NON_CLASS_FILE_CHECKLIST_SAVE_FILE);
        if (nonClassFileChecklistBytes == null) {
            return new HashMap<>(1);
        }
        String nonClassFileChecklist = new String(nonClassFileChecklistBytes, StandardCharsets.UTF_8);
        Logger.debug(DecryptExecutor.class, "nonClassFileChecklist -> " + nonClassFileChecklist);
        if (StrUtil.isBlank(nonClassFileChecklist)) {
            Logger.debug(DecryptExecutor.class, "nonClassFileChecklist is empty.");
            return new HashMap<>(1);
        }
        Set<String> needToDecryptNonClassFileSet = Arrays.stream(nonClassFileChecklist.split(Constant.COMMA)).collect(Collectors.toSet());
        // 检查密码一致性
        boolean userIfInputPwdWhileDecrypt = checkPwdConsistency(zipFilePath, Constant.DEFAULT_ENCRYPTED_BASE_SAVE_DIR, password);
        
        Map<String, byte[]> replaceMap = new HashMap<>(16);
        for (String zipEntryName : needToDecryptNonClassFileSet) {
            // 只处理有印章的
            byte[] cleanedBytes = IOUtil.readFileFromWorkbenchRoot(zipFile, zipEntryName);
            if (DecryptExecutor.verifySeal(cleanedBytes)) {
                byte[] encryptedBytes = IOUtil.readFileFromWorkbenchRoot(zipFile,
                        Constant.DEFAULT_ENCRYPTED_NON_CLASSES_SAVE_DIR + zipEntryName);
                byte[] bytes = EncryptUtil.decrypt(encryptedBytes, userIfInputPwdWhileDecrypt ? password :
                        obtainAutoGeneratedPwdWhileEncrypt(zipFilePath, Constant.DEFAULT_ENCRYPTED_BASE_SAVE_DIR));
                replaceMap.put(zipEntryName, bytes);
            }
        }
        Logger.debug(DecryptExecutor.class, "un-mask non-classes contains -> " + replaceMap.keySet());
        return JarUtil.rewriteZipEntry(new ZipFile(zipFilePath), replaceMap);
    }
    
    
    /**
     * 获取class-winter加密时自动生成的密码
     *
     * @param projectPath
     *            jar/war文件或目录（如: /tmp/, /tmp/abc.jar等）
     * @param classWinterInfoDir
     *            class-winter相关信息的存储目录
     * @return  密码
     */
    private static char[] obtainAutoGeneratedPwdWhileEncrypt(String projectPath, String classWinterInfoDir) {
        // 项目本身
        if (classWinterInfoDir.equals(Constant.DEFAULT_ENCRYPTED_BASE_SAVE_DIR)) {
            if (Cache.passwordCacheForDecrypt != null) {
                // 从缓存读取
                return Cache.passwordCacheForDecrypt;
            }
            // 读取文件
            byte[] passwordByte = IOUtil.readFileFromWorkbenchRoot(new File(projectPath), Constant.PWD_WINTER);
            if (passwordByte == null) {
                // jar包中没有Constant.PWD_WINTER指向的文件，说明在加密时，用户主动指定了密码，没有采用自动生成密码的机制
                throw new ClassWinterException("please input password.");
                
            }
            // 存时，是加密存进去的； 这里读取时，解密一下
            passwordByte = EncryptUtil.decrypt(new String(passwordByte, StandardCharsets.UTF_8),
                    Cache.sealCache.toCharArray()).getBytes(StandardCharsets.UTF_8);
            String password = new String(passwordByte, StandardCharsets.UTF_8);
            char[] passwordCharArr = password.toCharArray();
            // 放入缓存
            Cache.passwordCacheForDecrypt = passwordCharArr;
            return passwordCharArr;
        } else {
            // lib包
            if (Cache.libPasswordCache != null) {
                // 从缓存读取
                return Cache.libPasswordCache.get(classWinterInfoDir);
            }
            Cache.libPasswordCache = new HashMap<>(8);
            // 读取文件
            byte[] passwordByte = IOUtil.readFileFromWorkbenchRoot(new File(projectPath), classWinterInfoDir + Constant.PWD_WINTER_SIMPLE_NAME);
            // 在加密时就将lib的密码写入了的，这里不可能为null的
            Objects.requireNonNull(passwordByte);
            // 存时，是加密存进去的； 这里读取时，解密一下
            char[] passwordCharArr = EncryptUtil.decrypt(new String(passwordByte, StandardCharsets.UTF_8),
                    Cache.sealCache.toCharArray()).toCharArray();
            Cache.libPasswordCache.put(classWinterInfoDir, passwordCharArr);
            return passwordCharArr;
        }
    }
}
